package main

import (
	"fmt"
	"sync"
	"time"
)

/**
sync.Cond 从字面意思看是条件变量，它具有阻塞协程和唤醒协程的功能，所以可以在满足一定条件的情况下唤醒协程，但条件变量只是它的一种使用场景。

下面我以 10 个人赛跑为例来演示 sync.Cond 的用法。在这个示例中有一个裁判，裁判要先等这 10 个人准备就绪，然后一声发令枪响，这 10 个人就可以开始跑了，如下所示：
*/

func race() {
	cond := sync.NewCond(&sync.Mutex{})
	wg := sync.WaitGroup{}
	wg.Add(11)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			fmt.Println(num, "号准备就绪,等待裁判吹号...")
			cond.L.Lock()
			cond.Wait()
			fmt.Println(num, "号开跑！！！")
			cond.L.Unlock()
		}(i)
	}
	// 等待所有协程都处于Wait等待状态（等待所有选手准备就绪）
	time.Sleep(2 * time.Second)

	// 唤醒所有协程，裁判员发号指令
	fmt.Println("裁判员发号指令")
	cond.Broadcast()
	wg.Done()

	// 防止函数提前返回退出
	wg.Wait()
}

func race2() {
	cond := sync.NewCond(&sync.Mutex{})
	wg := sync.WaitGroup{}
	wg.Add(11)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			fmt.Println(num, "号准备就绪,等待裁判吹号...")
			cond.L.Lock()
			cond.Wait()
			fmt.Println(num, "号开跑！！！")
			cond.L.Unlock()
		}(i)
	}
	// 等待所有协程都处于Wait等待状态（等待所有选手准备就绪）
	time.Sleep(2 * time.Second)

	go func() {
		defer wg.Done()
		// 唤醒所有协程，裁判员发号指令
		fmt.Println("裁判员发号指令")
		cond.Broadcast()
	}()

	// 防止函数提前返回退出
	wg.Wait()
}

func race3() {
	cond := sync.NewCond(&sync.Mutex{})
	wg := sync.WaitGroup{}
	wg.Add(11)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			fmt.Println(num, "号准备就绪,等待裁判吹号...")
			//cond.L.Lock()
			cond.Wait() // fatal error: sync: unlock of unlocked mutex
			fmt.Println(num, "号开跑！！！")
			//cond.L.Unlock()
		}(i)
	}
	// 等待所有协程都处于Wait等待状态（等待所有选手准备就绪）
	time.Sleep(2 * time.Second)

	go func() {
		defer wg.Done()
		// 唤醒所有协程，裁判员发号指令
		fmt.Println("裁判员发号指令")
		cond.Broadcast()
	}()

	// 防止函数提前返回退出
	wg.Wait()
}

func race4() {
	var sum = 0
	cond := sync.NewCond(&sync.Mutex{})
	wg := sync.WaitGroup{}
	wg.Add(11)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			fmt.Println(num, "号准备就绪,等待裁判吹号...")
			cond.L.Lock() // Wait方法一定要加锁，不然会报错
			cond.Wait()
			sum += 1
			fmt.Println(num, "号开跑！！！")
			fmt.Println("sum:", sum) // 可以看到虽然是唤醒所有goroutine，但是因为加了锁，这段代码也是互斥的，运行顺序相当于串行
			cond.L.Unlock()
		}(i)
	}
	// 等待所有协程都处于Wait等待状态（等待所有选手准备就绪）
	time.Sleep(2 * time.Second)

	go func() {
		defer wg.Done()
		// 唤醒所有协程，裁判员发号指令
		fmt.Println("裁判员发号指令")
		cond.Broadcast()
	}()

	// 防止函数提前返回退出
	wg.Wait()
}

/**
以上示例中有注释说明，已经很好理解，我这里再大概讲解一下步骤：
1.通过 sync.NewCond 函数生成一个 *sync.Cond，用于阻塞和唤醒协程；
2.然后启动 10 个协程模拟 10 个人，准备就位后调用 cond.Wait() 方法阻塞当前协程等待发令枪响，这里需要注意的是调用 cond.Wait() 方法时要加锁；
3.time.Sleep 用于等待所有人都进入 wait 阻塞状态，这样裁判才能调用 cond.Broadcast() 发号施令；
4.裁判准备完毕后，就可以调用 cond.Broadcast() 通知所有人开始跑了。
*/
func main() {
	race4()
}

/**
sync.Cond 有三个方法，它们分别是：
Wait，阻塞当前协程，直到被其他协程调用 Broadcast 或者 Signal 方法唤醒，使用的时候需要加锁，使用 sync.Cond 中的锁即可，也就是 L 字段。
Signal，唤醒一个等待时间最长的协程。
Broadcast，唤醒所有等待的协程。
注意：在调用 Signal 或者 Broadcast 之前，要确保目标协程处于 Wait 阻塞状态，不然会出现死锁问题。
*/
